import { UploadStatus } from './constant';
import { computeFileHash, toPercent } from './utils';
import Http from './http';
import { IResponse } from './types';
import { FileCheckEntity, FileChunkEntity, UploadProgressCallback } from './interface';
import FileSplitReader from './FileSplitReader';
import TaskManager from './task';
import { MapType } from './types';

export enum FileNodeState {
  ERROR = -1,
  SELECTED = 0,
  COMPUTING = 1,
  UPLOADING = 2,
  COMPLETE = 3,
  WAITING = 4,
  SERVER_DEALING = 5,
}

export interface FileNode {
  id: string | number;
  status: number;
  file: File;
  name: string;
  size: number;
  lastModified: number;
}

export interface SuccessParams {
  node: FileNode;
}

export interface FailParams {
  node: FileNode;
}

export interface CompleteParams {
  node: FileNode;
  status: any;
  response: IResponse<any>;
}

export interface ErrorParams {
  node: FileNode;
  response: IResponse<any>;
}

export interface ProgressParams {
  node: FileNode;
  progress: number;
  percent: string;
}

export interface BeforeComputeHashParams {
  node: FileNode;
}

export interface HashComputingParams {
  node: FileNode;
  progress: number;
  percent: string;
}

export interface ComputeHashParams {
  node: FileNode;
  hash: string;
}

export interface BeforeUploadParams {
  node: FileNode;
}

export interface UploaderOptions {
  limit: number;
  chunkSize: number;
  customParams: MapType<any>;
  checkUrl: string;
  uploadUrl: string;
  concatUrl: string;

  onBeforeUpload(params: BeforeUploadParams): void;

  onCheckError(err: any): void;

  onProgress(params: ProgressParams): void;

  onBeforeConcat(params: BeforeUploadParams): void;

  onUploaded(params: SuccessParams): void;

  onError(params: ErrorParams): void;

  onSuccess(params: SuccessParams): void;

  onComplete(params: CompleteParams): void;

  onFail(params: FailParams): void;

  onBeforeHashCompute(params: BeforeComputeHashParams): void;

  onHashComputing(params: HashComputingParams): void;

  onHashComputed(params: ComputeHashParams): void;

  onHashComputeError(err: any): void;
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
function noop(...args: any | any[]): any {}

export function check(options: { url: string; data: MapType<any> }) {
  const { url, data } = options;
  return Http.get<IResponse<FileCheckEntity>>({
    url,
    data: {
      ...data,
    },
  }).then((res) => res.data);
}

export function uploadBlob(options: any) {
  const { url, data, onUpload, blobSize } = options;
  return Http.post({
    url,
    data,
    contentType: 'multipart/form-data',
    uploadListener: {
      progress: (event) => {
        onUpload && onUpload((event.loaded / event.total) * blobSize, blobSize);
      },
    },
  })
    .then((res) => res.data)
    .then(() => {
      // 上传
      return {
        state: true,
        message: 'ok',
        error: null,
      };
    });
}

export function concat(options: { url: string; data: MapType<any> }) {
  const { url, data } = options;
  return Http.get<IResponse<null>>({
    url,
    data: {
      ...data,
    },
  }).then((res) => res.data);
}

export function getUploadQueue(chunkCount: number, chunks: FileChunkEntity[]) {
  const temp = new Array(chunkCount).fill(0).map((_, i) => i);
  const queue: number[] = [];
  const map: Record<string, boolean> = {};
  // compute file uploaded size
  let uploadedSize = 0;
  chunks.forEach((i: FileChunkEntity) => {
    map[i.index] = true;
    uploadedSize += i.size;
  });
  // compute pending upload queue
  [...temp].forEach((_, i) => {
    if (!map[i]) {
      queue.push(i);
    }
  });
  return {
    uploadedSize,
    queue,
  };
}

export function normalizeUploadOptions(options: Partial<UploaderOptions>) {
  return {
    limit: 3,
    chunkSize: 1024 * 1024 * 4,
    checkUrl: '',
    uploadUrl: '',
    concatUrl: '',
    customParams: {},
    onBeforeUpload: noop,

    onCheckError: noop,

    onProgress: noop,

    onBeforeConcat: noop,

    onUploaded: noop,

    onError: noop,

    onSuccess: noop,

    onComplete: noop,

    onFail: noop,

    onBeforeHashCompute: noop,

    onHashComputing: noop,

    onHashComputed: noop,

    onHashComputeError: noop,
    ...options,
  };
}

export function uploader(taskManager: TaskManager, uploaderOptions: UploaderOptions) {
  const options = normalizeUploadOptions(uploaderOptions);
  return async (fileNode: FileNode) => {
    const {
      chunkSize,
      checkUrl,
      uploadUrl,
      concatUrl,
      customParams,
      onBeforeHashCompute,
      onHashComputing,
      onHashComputeError,
      onBeforeUpload,
      onProgress,
      onBeforeConcat,
      onSuccess,
      onComplete,
      onError,
    } = options;
    return new Promise<CompleteParams>(async (resolve, reject) => {
      // origin file object
      const file = fileNode.file;
      // computing
      // publish status
      onBeforeHashCompute({ node: { ...fileNode, status: UploadStatus.COMPUTING } });
      // 计算 md5
      const fileMd5 = await computeFileHash(file, {
        progress: (progress, percent) => {
          onHashComputing({ progress, percent, node: { ...fileNode, status: UploadStatus.COMPUTING } });
        },
        error(err) {
          onHashComputeError(err);
        },
      });
      // uploading
      onBeforeUpload({ node: { ...fileNode, status: UploadStatus.UPLOADING } });
      // prepare
      const size = file.size;
      const response = await check({
        url: checkUrl,
        data: {
          ...customParams,
          record: true,
          hash: fileMd5,
          chunkSize,
          size,
          origin: file.name,
        },
      });
      const { data } = response;
      // uploaded
      if (data.uploaded) {
        onComplete({ node: fileNode, status: taskManager.getStatus(), response });
        resolve({ node: fileNode, status: taskManager.getStatus(), response });
        return;
      }
      // compute chunk
      const reader = new FileSplitReader({
        file,
        chunkSize,
      });
      const chunkCount = reader.getChunkCount();
      // file total size
      const totalSize = size;
      let { queue, uploadedSize } = getUploadQueue(chunkCount, data.chunks);
      // concat chunks
      if (!queue.length) {
        onBeforeConcat({ node: { ...fileNode, status: UploadStatus.SERVER_DEALING } });
        const response = await concat({
          url: concatUrl,
          data: {
            ...customParams,
            hash: fileMd5,
          },
        });
        const { code } = response;
        if (code === 0) {
          onComplete({ node: fileNode, status: taskManager.getStatus(), response });
          resolve({ node: fileNode, status: taskManager.getStatus(), response });
        } else {
          onError && onError({ node: fileNode, response });
          reject({ node: fileNode });
        }
        return;
      }
      // do upload
      const upload = queue;
      const type = file.type;

      function newTask(index: number, onUpload: UploadProgressCallback) {
        return TaskManager.newTask({
          id: index + '',
          callback: async () => {
            const { data } = await reader.read(index);
            const blob = new Blob([data], { type });
            const postData = {
              ...customParams,
              file: blob,
              id: index,
              chunks: chunkCount,
              fileMd5,
            };
            return uploadBlob({ url: uploadUrl, data: postData, blobSize: blob.size, onUpload });
          },
        });
      }

      taskManager
        .clearCallback()
        .complete(async () => {
          // '上传报告', '分片已上传完成！');
          const status = taskManager.getStatus();
          if (!status.ok) {
            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
            // @ts-ignore
            onComplete({ node: fileNode, status });
            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
            // @ts-ignore
            resolve({ node: fileNode, status });
            return;
          }
          taskManager.clean();
          const response = await concat({
            url: concatUrl,
            data: {
              ...customParams,
              hash: fileMd5,
            },
          });
          const { code } = response;
          if (code === 0) {
            onComplete({ node: fileNode, status: taskManager.getStatus(), response });
            resolve({ node: fileNode, status: taskManager.getStatus(), response });
          } else {
            // notice.error('上传报告', message);
            onError && onError({ node: fileNode, response });
            reject({ node: fileNode });
          }
        })
        .register(
          upload.map((i) =>
            newTask(i, (loaded) => {
              uploadedSize += loaded;
              const progress = uploadedSize / totalSize;
              onProgress({ node: fileNode, percent: toPercent(progress), progress });
            })
          )
        )
        .start();
    });
  };
}

export default class Uploader {
  static upload = uploader;
  private isUploading = false;
  private files = [] as FileNode[];
  private readonly taskManager: TaskManager;
  private readonly props: UploaderOptions;
  private readonly doUpload: (node: FileNode) => void;

  constructor(options: Partial<UploaderOptions>) {
    this.props = normalizeUploadOptions(options);
    this.taskManager = TaskManager.new({
      limit: this.props.limit,
    });
    this.doUpload = uploader(this.taskManager, this.props);
  }

  start = async () => {
    const { files, isUploading } = this;
    if (isUploading) {
      return;
    }
    // 队列中待上传的文件
    const pendingFiles = files.filter((i) => i.status !== UploadStatus.COMPLETE);
    if (!pendingFiles.length) {
      return { state: false, statusText: '没有选择可上传的文件' };
    }
    this.isUploading = true;
    for (const file of pendingFiles) {
      await this.upload(file);
    }
    this.isUploading = false;
    return {
      state: true,
      message: '队列已全部完成',
    };
  };

  upload(node: FileNode) {
    return this.doUpload(node);
  }
}
